/**
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.api.adwords.starterkit.examples;

import com.google.api.adwords.lib.AdWordsService;
import com.google.api.adwords.lib.AdWordsUser;
import com.google.api.adwords.lib.utils.MapUtils;
import com.google.api.adwords.starterkit.StarterKit;
import com.google.api.adwords.starterkit.settings.AdGroupSettings;
import com.google.api.adwords.starterkit.settings.CampaignSettings;
import com.google.api.adwords.starterkit.settings.KeywordSettings;
import com.google.api.adwords.starterkit.settings.StarterKitSettings;
import com.google.api.adwords.starterkit.settings.TextAdSettings;
import com.google.api.adwords.starterkit.utils.AdTemplate;
import com.google.api.adwords.starterkit.utils.DumpExceptions;
import com.google.api.adwords.starterkit.utils.JSONUtils;
import com.google.api.adwords.starterkit.wrappers.AdGroupWrapper;
import com.google.api.adwords.starterkit.wrappers.CampaignWrapper;
import com.google.api.adwords.v201101.o.CompetitionSearchParameterLevel;
import com.google.api.adwords.v201101.o.IdeaTextMatchesSearchParameter;
import com.google.api.adwords.v201101.cm.ApiException;
import com.google.api.adwords.v201101.cm.Keyword;
import com.google.api.adwords.v201101.cm.KeywordMatchType;
import com.google.api.adwords.v201101.cm.Paging;
import com.google.api.adwords.v201101.o.Attribute;
import com.google.api.adwords.v201101.o.AttributeType;
import com.google.api.adwords.v201101.o.CompetitionSearchParameter;
import com.google.api.adwords.v201101.o.CriterionAttribute;
import com.google.api.adwords.v201101.o.IdeaType;
import com.google.api.adwords.v201101.o.KeywordAttribute;
import com.google.api.adwords.v201101.o.KeywordMatchTypeSearchParameter;
import com.google.api.adwords.v201101.o.RelatedToKeywordSearchParameter;
import com.google.api.adwords.v201101.o.RelatedToUrlSearchParameter;
import com.google.api.adwords.v201101.o.RequestType;
import com.google.api.adwords.v201101.o.SearchParameter;
import com.google.api.adwords.v201101.o.TargetingIdea;
import com.google.api.adwords.v201101.o.TargetingIdeaPage;
import com.google.api.adwords.v201101.o.TargetingIdeaSelector;
import com.google.api.adwords.v201101.o.TargetingIdeaServiceInterface;
import com.mindprod.entities.StripEntities;

import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.XML;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.rmi.RemoteException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;

import javax.xml.rpc.ServiceException;

/**
 * The Class RSSAds is a demo application that Creates campaigns based on the 
 * content of a RSS feed
 * 
 * @author rodrigo.f.vale@gmail.com  (Rodrigo Vale)
 */
public class RSSAds {
  public static final int NO_MATCH = 0;
  public static final int BROAD_MATCH = 1;
  public static final int EXACT_MATCH = 2;
  private static final Logger log = Logger.getLogger("StarterKit");

  /**
   * The Class LineSplit. It supports the process of splitting a title of a news
   */
  public class LineSplit {
    public String line = "";
    public int match = NO_MATCH; 
    public boolean error = false;
  }

  /**
   * The Class TitleSplit. It supports the process of splitting a title of a 
   * news
   */
  public class TitleSplit {
    public LineSplit line1;
    public LineSplit line2;
  }

  /**
   * How to use.
   */
  public void howToUse() {
    System.out.print(
        "SYNOPSIS\n" +
        "       RSSAds [SETTINGS]\n" +
        "\n" +
        "DESCRIPTION\n" +
        "       This demo creates campaigns based on the content of a RSS\n" +
        "URL.\n" +
        "\n" +
        "EXAMPLE\n" +
        "       java starterkit.jar RSSAds rssads_settings.xml\n");
    System.exit(0);
  }

  /**
   * Split. A support method to split the title in two peaces. 
   *
   * @param line the line to be splitted
   * @param size1 the size of the new line
   * @return the new line with defined size
   */
  public LineSplit split(String line, int size1) {
    LineSplit lineSplit = new LineSplit();
    int lastSpace = 0;

    if (size1 == 0) {
      return lineSplit;
    }

    if ((size1 > 0) && (line.length() == 0)) {
      lineSplit.line = line;
      lineSplit.match = NO_MATCH;
      return lineSplit;
    }

    if ((size1 > 0) && (line.length() <= size1)) {
      lineSplit.line = line;
      lineSplit.match = EXACT_MATCH;
      return lineSplit;
    }

    lineSplit.match = BROAD_MATCH;
    for (int i = 0; i < line.length() && (i <= size1); i++) {
      char c = line.charAt(i);
      if (c == ' ') {
        lastSpace = i;
      }
    }
    if (lastSpace > 0) {
      lineSplit.line = line.substring(0, lastSpace);
    } else {
      lineSplit.error = true;
      return lineSplit;
    }
    return lineSplit;
  }

  /**
   * Split. The main method used to split a title in two small peaces
   *
   * @param title the title of the news
   * @param size1 the size of the first line
   * @param size2 the size of the second line
   * @return the split title
   */
  public TitleSplit split(String title, int size1, int size2) {
    TitleSplit titleSplit = new TitleSplit();

    LineSplit lineSplit1 = split(title, size1);
    titleSplit.line1 = lineSplit1;
    if (!lineSplit1.error) {
      titleSplit.line1 = lineSplit1;
      String subTitle = "";
      if (!lineSplit1.line.equals(title)) {
        if (lineSplit1.line.length() > 0) {
          subTitle = title.substring(lineSplit1.line.length() + 1);
        } else {
          subTitle = title;
        }
      }
      LineSplit lineSplit2 = split(subTitle, size2);
      titleSplit.line2 = lineSplit2; 
    }
    return titleSplit;
  }

  /**
   * Gets the matches.
   *
   * @param line the line
   * @return the matches
   */
  public int getMatches(String line) {
    int match = NO_MATCH;

    if (line.contains("[T]...")) {
      match = BROAD_MATCH;
    } else if (line.contains("[T]")) {
      match = EXACT_MATCH;
    } else {
      match = NO_MATCH;
    }
    return match;
  }

  /**
   * Gets the matches.
   *
   * @param template the textad template
   * @return the matches
   */
  public int[] getMatches(AdTemplate template) {
    int[] matches = new int[2];

    matches[0] = getMatches(template.getLine1());
    matches[1] = getMatches(template.getLine2());

    if ((matches[0] == EXACT_MATCH) && (matches[1] != NO_MATCH)) {
      matches[0] = BROAD_MATCH;
    }

    return matches;
  }

  /**
   * Gets the lines size.
   *
   * @param template the template
   * @return the lines size
   */
  public static int[] getLinesSize(AdTemplate template) {
    int[] sizes = new int[2];

    String line1 = template.getLine1().replace("[T]", "");
    String line2 = template.getLine2().replace("[T]", "");
    sizes[0] = 35 - line1.length();
    if (line1.equals(template.getLine1())) {
      sizes[0] = 0;
    } else {
      sizes[0] = 35 - line1.length();
    }
    if (line2.equals(template.getLine2())) {
      sizes[1] = 0;
    } else {
      sizes[1] = 35 - line2.length();
    }

    return sizes;
  }

  /**
   * The main method.
   *
   * @param args the arguments
   * @throws JSONException the jSON exception
   * @throws ApiException the api exception
   * @throws RemoteException the remote exception
   * @throws ServiceException the service exception
   */
  public static void main(String[] args) throws JSONException, ApiException, 
      RemoteException, ServiceException {
    RSSAds adExpress =  new RSSAds();

    if (args.length < 1) {
      adExpress.howToUse();
    }

    adExpress.update(args[0]);
  }

  /**
   * Update.
   *
   * @param fileSettings the file settings
   * @throws JSONException the jSON exception
   * @throws ApiException the api exception
   * @throws RemoteException the remote exception
   * @throws ServiceException the service exception
   */
  public void update(String fileSettings) throws JSONException, ApiException, 
      RemoteException, ServiceException {
    StarterKitSettings starterKitSettings 
      = new StarterKitSettings(fileSettings);
    CampaignSettings campaignSettings 
      = starterKitSettings.getCampaignSettings();
    AdGroupSettings adGroupSettings = starterKitSettings.getAdGroupSettings();
    KeywordSettings keywordSettings = starterKitSettings.getKeywordSettings();
    TextAdSettings textAdSettings = starterKitSettings.getTextAdSettings();

    StarterKit starterKit = new StarterKit(starterKitSettings);
    starterKit.load();
    AdWordsUser user = starterKit.getAdWordsUser();

    // loading RSSFeed settings at the end of rssfeed_settings.xml
    JSONObject jsonSettings = starterKitSettings.getJSONSettings();
    JSONObject jsonRSSFeed = jsonSettings.getJSONObject("RSSFeed");
    JSONObject jsonRSSFeedKeyword = jsonRSSFeed.getJSONObject("Keyword");
    String rss = jsonRSSFeed.getJSONObject("RSS").getString("value");
    int keywordMultiplier 
        = jsonRSSFeedKeyword.getJSONObject("KeywordMultiplier").getInt("value");
    JSONArray keywordCompetitions = 
        JSONUtils.getJSONArray(jsonRSSFeedKeyword, "KeywordCompetitions", 
            "KeywordCompetition");
    
    CompetitionSearchParameterLevel competitionSearchParameterLevel[] = null;
    if (keywordCompetitions.length() > 0) {
      competitionSearchParameterLevel = 
          new CompetitionSearchParameterLevel[keywordCompetitions.length()];
      for (int i = 0; i < keywordCompetitions.length(); i++) {
        String keywordCompetition = keywordCompetitions.getString(i);
        competitionSearchParameterLevel[i] = 
            CompetitionSearchParameterLevel.fromString(keywordCompetition);
      }   
    }
    String campaignName = jsonRSSFeed.getJSONObject("Category")
        .getString("value");
    Double minCtr = jsonRSSFeed.getJSONObject("MinCtr").getDouble("value");
    int minImp = jsonRSSFeed.getJSONObject("MinImp").getInt("value");    
    long lifeTime = jsonRSSFeed.getJSONObject("LifeTime").getLong("value");
    String encode = jsonRSSFeed.getJSONObject("Encode").getString("value");

    // download RSS
    String rssContent = downloadTextContent(rss, encode);
    JSONObject jsonRSS = XML.toJSONObject(rssContent);
    JSONArray jsonItems = jsonRSS.getJSONObject("rss")
        .getJSONObject("channel").getJSONArray("item");

    // campaign is the RSS category
    CampaignWrapper campaignWrapper = 
      starterKit.getCampaignWrapper(campaignName, campaignSettings);

    for (int i = 0; i < jsonItems.length(); i++) {
      // getting the news title and URL
      JSONObject jsonItem = jsonItems.getJSONObject(i);
      String title = jsonItem.getString("title");
      title = StripEntities.stripHTMLEntities(title, ' ');
      title = StripEntities.stripHTMLTags(title);
      title = title.replaceAll("\\s+", " ");
      log.info("Processing: " + title);
      
      String url = jsonItem.getString("link");
      String adGroupName = title;

      // AdGroup is the title of the news
      AdGroupWrapper adGroupWrapper = campaignWrapper
          .getAdGroupWrapper(adGroupName, adGroupSettings);

      // keywords are the generated by RelatedToUrlSearchParameter service
      // and by RelatedToKeywordSearchParameter service depending on the number
      // of keywordMultiplaier setting
      HashSet<Keyword> keywordsFeed = getKeywordsFromUrl(user, url, 
          competitionSearchParameterLevel);
      HashSet<Keyword> keywords = new HashSet<Keyword>();
      keywords.addAll(keywordsFeed);
      
      // new keywords will feed RelatedToKeywordSearchParameter service
      // based on the keywordMultiplaier setting
      if (keywordsFeed.size() > 0) {
        for (int m = 0; m < keywordMultiplier; m++) {
          keywordsFeed = getKeywordsFromKeywords(user, keywordsFeed, 
              competitionSearchParameterLevel);
          keywords.addAll(keywordsFeed);
        }
      }
      log.info("Found " + keywords.size() + " keywords for this news");
      
      for (final AdTemplate template : starterKitSettings.getTextAdSettings()
          .getAdTemplates()) {
        int[] sizes = getLinesSize(template);
        int[] matches = getMatches(template);
        TitleSplit titleSplit = split(title, sizes[0], sizes[1]);

        if ((!titleSplit.line1.error) && (!titleSplit.line2.error)) {
          if ((matches[0] == titleSplit.line1.match) 
              && (matches[1] == titleSplit.line2.match)) {
            template.replaceAllDesc1("[T]", titleSplit.line1.line);
            template.replaceAllDesc2("[T]", titleSplit.line2.line);
            adGroupWrapper.getTextWrapper(template, textAdSettings);
            log.info("Using TextAd: " + template.getHead() + "\t" + 
                template.getLine1() + "\t" + template.getLine2() + "\t" +
                template.getVisibleUrl() + "\t" + template.getDestUrl());
          }
        }
      }

      for (Keyword keyword : keywords) {
        adGroupWrapper.getKeywordWrapper(keyword.getText(), false, url, 
            keywordSettings);
      }
    }  

    // get access to DB and pause keywords that with lower Ctr
    // and news that are too old
    Connection conn = starterKit.getConnection();
    Date date = new Date();
    long now = date.getTime();    
    PreparedStatement prep;
    try {
      prep = conn.prepareStatement(
          "UPDATE Keywords SET Status = 'PAUSED', lastUpdate = ?, " +
          "IsTouched = 1, IsUpdated = 1 " +
          "WHERE Ctr < ? AND Impressions > ?");
      prep.setLong(1, now);
      prep.setDouble(2, minCtr);
      prep.setDouble(3, minImp);
      prep.executeUpdate();
    } catch (SQLException e) {
      DumpExceptions.dumpSQLException(e);
    }        

    try {
      prep = conn.prepareStatement(
          "UPDATE AdGroups SET status = 'PAUSED', lastUpdate = ?, " +
          "IsTouched = 1, IsUpdated = 1 " +
          "WHERE (? - ?) > lastUpdate");
      prep.setLong(1, now);
      prep.setLong(2, now);
      prep.setLong(3, lifeTime);
      prep.executeUpdate();
    } catch (SQLException e) {
      DumpExceptions.dumpSQLException(e);
    }        

    // updates changes on DB
    starterKit.save();
    starterKit.done();
  }
  
  /**
   * Download a text content to a URL.
   */  
  private String downloadTextContent(String url, String charset) {
    StringBuffer s = new StringBuffer();
    if ((charset == null) || charset.length() == 0) {
      charset = "utf-8";
    }
    String rLine = null;

    URL intenralUrl;
    try {
      intenralUrl = new URL(url);

      URLConnection conn = intenralUrl.openConnection();
      conn.setDoOutput(true);
      InputStream in = null;
      in = intenralUrl.openStream();

      BufferedReader bReader 
      = new BufferedReader(new InputStreamReader(in,charset));
      while ( (rLine = bReader.readLine()) != null) {
        String tmp_rLine = rLine;
        int str_len = tmp_rLine.length();
        if (str_len > 0) {
          s.append(tmp_rLine);
        }
        tmp_rLine = null;
      }
      in.close();
    } catch (MalformedURLException e) {
      DumpExceptions.dumpMalformedURLException(e);
    } catch (IOException e) {
      DumpExceptions.dumpIOException(e);
    }
    return s.toString();
  }
  
  /**
   * Gets the keywords from url using the AdWords API Targeting Idea Service.
   *
   * @param user the AdWords user
   * @param url the url to feed the Targeting Idea Service
   * @return a set of keywords based on the url content
   */
  public static HashSet<Keyword> getKeywordsFromUrl(AdWordsUser user, 
      String url, 
      CompetitionSearchParameterLevel competitionSearchParameterLevel[]) {
    // Get the TargetingIdeaService.
    TargetingIdeaServiceInterface targetingIdeaService;
    HashSet<Keyword> keywords = new HashSet<Keyword>();
    try {
      targetingIdeaService 
        = user.getService(AdWordsService.V201101.TARGETING_IDEA_SERVICE);

      // Create selector.
      TargetingIdeaSelector selector = new TargetingIdeaSelector();
      selector.setRequestType(RequestType.IDEAS);
      selector.setIdeaType(IdeaType.KEYWORD);
      selector.setRequestedAttributeTypes(new AttributeType[] {
          AttributeType.CRITERION,
          AttributeType.AVERAGE_TARGETED_MONTHLY_SEARCHES});

      RelatedToUrlSearchParameter relatedToUrlSearchParameter 
          = new RelatedToUrlSearchParameter();
      relatedToUrlSearchParameter.setUrls(new String[] {url});
      
      if (competitionSearchParameterLevel != null) {
        CompetitionSearchParameter competitionSearchParameter 
            = new CompetitionSearchParameter();
        competitionSearchParameter.setLevels(competitionSearchParameterLevel);
        selector.setSearchParameters(
            new SearchParameter[] {relatedToUrlSearchParameter,
                competitionSearchParameter});
      } else {
        selector.setSearchParameters(
            new SearchParameter[] {relatedToUrlSearchParameter});        
      }
      // Set selector paging (required for targeting idea service).
      Paging paging = new Paging();
      paging.setStartIndex(0);
      paging.setNumberResults(20);
      selector.setPaging(paging);

      // Get related keywords.
      TargetingIdeaPage page = targetingIdeaService.get(selector); 

      // Display related keywords.
      if (page.getEntries() != null && page.getEntries().length > 0) {
        for (TargetingIdea targetingIdea : page.getEntries()) {
          Map<AttributeType, Attribute> data 
              = MapUtils.toMap(targetingIdea.getData());
          Keyword keyword = (Keyword) ((CriterionAttribute) 
              data.get(AttributeType.CRITERION)).getValue();
          keywords.add(keyword);
        }
      } else {
        log.info("No related keywords were found.");
      }
    } catch (ServiceException e) {
      DumpExceptions.dumpServiceException(e);
    } catch (ApiException e) {
      DumpExceptions.dumpApiException(e);
    } catch (RemoteException e) {
      DumpExceptions.dumpRemoteException(e);
    }
    return keywords;
  }

  /**
   * Use the Target Idea Service to create new keywords based on a set of 
   * keywords feeds 
   *
   * @param user the AdWords user
   * @param keywords the set of keywords feed
   * @return the suggested new keywords 
   */
  public static HashSet<Keyword> getKeywordsFromKeywords(AdWordsUser user, 
      HashSet<Keyword> keywords, 
      CompetitionSearchParameterLevel competitionSearchParameterLevel[]) {
    // Get the TargetingIdeaService.
    TargetingIdeaServiceInterface targetingIdeaService;
    HashSet<Keyword> newKeywords = new HashSet<Keyword>(); 
    try {
      targetingIdeaService 
          = user.getService(AdWordsService.V201101.TARGETING_IDEA_SERVICE);

      // Create selector.
      TargetingIdeaSelector selector = new TargetingIdeaSelector();
      selector.setRequestType(RequestType.IDEAS);
      selector.setIdeaType(IdeaType.KEYWORD);
      selector.setRequestedAttributeTypes(new AttributeType[] {
          AttributeType.CRITERION,
          AttributeType.AVERAGE_TARGETED_MONTHLY_SEARCHES});

      // Set selector paging (required for targeting idea service).
      Paging paging = new Paging();
      paging.setStartIndex(0);
      paging.setNumberResults(10);
      selector.setPaging(paging);

      // Create related to keyword search parameter.
      RelatedToKeywordSearchParameter relatedToKeywordSearchParameter =
          new RelatedToKeywordSearchParameter();
      relatedToKeywordSearchParameter
          .setKeywords(keywords.toArray(new Keyword[0]));

      // Create keyword match type search parameter to ensure unique results.
      KeywordMatchTypeSearchParameter keywordMatchTypeSearchParameter =
          new KeywordMatchTypeSearchParameter();
      keywordMatchTypeSearchParameter.setKeywordMatchTypes(
          new KeywordMatchType[] {KeywordMatchType.BROAD});
      
      if (competitionSearchParameterLevel != null) {
        CompetitionSearchParameter competitionSearchParameter 
            = new CompetitionSearchParameter();
        competitionSearchParameter.setLevels(competitionSearchParameterLevel);
        selector.setSearchParameters(
            new SearchParameter[] {keywordMatchTypeSearchParameter,
                competitionSearchParameter, relatedToKeywordSearchParameter});
      } else {
        selector.setSearchParameters(
            new SearchParameter[] {keywordMatchTypeSearchParameter});        
      }

      // Get related keywords.
      TargetingIdeaPage page = targetingIdeaService.get(selector);

      // Display related keywords.
      if (page.getEntries() != null && page.getEntries().length > 0) {
        for (TargetingIdea targetingIdea : page.getEntries()) {
          Map<AttributeType, Attribute> data 
              = MapUtils.toMap(targetingIdea.getData());
          Keyword keyword = (Keyword) ((CriterionAttribute) 
              data.get(AttributeType.CRITERION)).getValue();
          newKeywords.add(keyword);
        }
      } else {
        log.info("No related keywords were found.");
      }
    } catch (ServiceException e) {
      DumpExceptions.dumpServiceException(e);
    } catch (ApiException e) {
      DumpExceptions.dumpApiException(e);
    } catch (RemoteException e) {
      DumpExceptions.dumpRemoteException(e);
    }
    return newKeywords;
  }
}